﻿using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;

/**
 * A base Environment for our regular gameplay.
 * May be extended for levels that work differently,
 * if that ever happens.
 */
public class LevelBase : EnvBase
{
    // Member vars
    protected FollowerCamera m_camera;
    protected List<CollidableObject> m_objectList;
    protected List<CollidableObject> m_additionList;
    protected List<SpawnPoint> m_SPList;
    protected Hero m_hero;
    protected QuinHero m_qhero;
    protected bool waitingRes = false;
    protected ProfessorObject m_prof;
    protected IncubatorObject m_incubator;
    protected Steve m_steve;
    protected HUD m_hud;
    protected double deadWait = 2000;
    protected double deadSince = 0;
    protected Vector3 heroStart = new Vector3(7500, 5000, 200);
    protected Vector3 steveStart = new Vector3(700, 4000, 300);
    private double roarTime = 10000;
    private double currentTime = 0;
    private int currentRoar = 0;

    protected GoalTrigger m_goalTrigger;

    /**
     * Our constructor.
     */
    public LevelBase(Levels.List level)
    {
        Game.getGame().saveData();

        Game.getGame().getSoundManager().stopSound();
        Game.getGame().getSoundManager().playSound(((AudioAsset)Game.getGame().getAssetLibrary().getAudio(AssetLibrary.audioType.GAME_MUSIC).get("gameMusic")).sound, 0.8f);

        //this is where we create the map
        m_objectList = new List<CollidableObject>();
        m_additionList = new List<CollidableObject>();

        // Load our spawnPoints from file.
        m_SPList = EnvBase.LoadSpawnList(level);

        // Pull Steve and Hero spawns from our spawnList
        SpawnPoint heroSpawn = null;
        SpawnPoint steveSpawn = null;
        SpawnPoint goalTrigger = null;
        foreach (SpawnPoint sp in m_SPList)
        {
            if (sp.getType() == ObjectFactory.TypeEnum.QuinHero)
            {
                m_qhero = (QuinHero)sp.spawn();
                heroSpawn = sp;
                heroStart = m_qhero.position;
            }
            else if (sp.getType() == ObjectFactory.TypeEnum.Steve)
            {
                m_steve = (Steve)sp.spawn();
                steveSpawn = sp;
                steveStart = m_steve.position;
            }
            else if (sp.getType() == ObjectFactory.TypeEnum.LoadingZone)
            {
                m_goalTrigger = (GoalTrigger)sp.spawn();
                goalTrigger = sp;
            }
            else if (m_prof == null && sp.getType() == ObjectFactory.TypeEnum.Professor)
            {
                m_prof = (ProfessorObject)sp.spawn();
                m_objectList.Add(m_prof);
            }
            else if (m_incubator == null && sp.getType() == ObjectFactory.TypeEnum.Incubator)
            {
                m_incubator = (IncubatorObject)sp.spawn();
                m_objectList.Add(m_incubator);
            }
        }
        if (heroSpawn != null)
        {
            m_SPList.Remove(heroSpawn);
        }
        if (steveSpawn != null)
        {
            m_SPList.Remove(steveSpawn);
        }
        if(goalTrigger != null)
        {
            m_SPList.Remove(goalTrigger);
        }

        m_hud = new HUD();
        m_camera = new FollowerCamera(new Vector3(0, 0, 0), m_qhero);

        m_goalTrigger.addGoal(LightObject.Type.Television);
        m_goalTrigger.addGoal(LightObject.Type.Laptop);
        m_goalTrigger.addGoal(LightObject.Type.Bear);
        m_goalTrigger.addGoal(DriveableObject.Type.Truck);
        m_goalTrigger.addGoal(UseableObject.Type.Dumpster);
        m_goalTrigger.addGoal(UseableObject.Type.Fridge);
    }

    public static void addSpawnPoint(SpawnPoint sp)
    {
        ((LevelBase)Game.getGame().getEnvStack().currentEnv()).m_SPList.Add(sp);
    }

    public static void addObject(CollidableObject co)
    {
        ((LevelBase)Game.getGame().getEnvStack().currentEnv()).m_additionList.Add(co);
    }

    /**
     * Get an instance of our HUD.
     */
    public HUD getHUD() { return m_hud; }

    /**
     * Returns a list of objects that collide with the default bounds of this object.
     */
    public static List<CollidableObject> collidesWith(CollidableObject co)
    {
        return collidesWith(co.collisionMin, co.collisionMax, co);
    }
    /**
     * Returns a list of objects that collide with the given bounds for some object.
     * Removes the given object from the list we're returning.
     */
    public static List<CollidableObject> collidesWith(Vector3 topLeftFront, Vector3 bottomRightBack, CollidableObject co)
    {
        QuinHero qhero = ((LevelBase)Game.getGame().getEnvStack().currentEnv()).m_qhero;
        Steve steve = ((LevelBase)Game.getGame().getEnvStack().currentEnv()).m_steve;
        List<CollidableObject> returnList = new List<CollidableObject>();
        foreach (CollidableObject it in ((LevelBase)Game.getGame().getEnvStack().currentEnv()).m_objectList)
        {
            if (it != co &&
                !(it is TriggerObject) &&
                !(it.collisionMax.X < topLeftFront.X ||
                it.collisionMin.X > bottomRightBack.X ||
                it.collisionMax.Y < topLeftFront.Y ||
                it.collisionMin.Y > bottomRightBack.Y ||
                it.collisionMax.Z < topLeftFront.Z ||
                it.collisionMin.Z > bottomRightBack.Z))
            {
                returnList.Add(it);
            }
        }
        if (!qhero.isDead &&
                qhero != co &&
                !(qhero.collisionMax.X < topLeftFront.X ||
                qhero.collisionMin.X > bottomRightBack.X ||
                qhero.collisionMax.Y < topLeftFront.Y ||
                qhero.collisionMin.Y > bottomRightBack.Y ||
                qhero.collisionMax.Z < topLeftFront.Z ||
                qhero.collisionMin.Z > bottomRightBack.Z))
        {
            returnList.Add(qhero);
        }
        if (steve != co &&
                 !(steve.collisionMax.X < topLeftFront.X ||
                 steve.collisionMin.X > bottomRightBack.X ||
                 steve.collisionMax.Y < topLeftFront.Y ||
                 steve.collisionMin.Y > bottomRightBack.Y ||
                 steve.collisionMax.Z < topLeftFront.Z ||
                 steve.collisionMin.Z > bottomRightBack.Z))
        {
            returnList.Add(steve);
        }
        return returnList;
    }

    /**
     * Returns a list of objects that this object is contained within.
     * Contained means the object is within the X and Z bounds of our object, and
     * is touching our object on the Y axis.
     */
    public static List<CollidableObject> contains(CollidableObject co)
    {
        Vector3 collisionMin = co.collisionMin;
        Vector3 collisionMax = co.collisionMax;
        QuinHero qhero = ((LevelBase)Game.getGame().getEnvStack().currentEnv()).m_qhero;
        Steve steve = ((LevelBase)Game.getGame().getEnvStack().currentEnv()).m_steve;
        List<CollidableObject> returnList = new List<CollidableObject>();
        foreach (CollidableObject it in ((LevelBase)Game.getGame().getEnvStack().currentEnv()).m_objectList)
        {
            if (it != co &&
                it.collisionMin.X > collisionMin.X &&
                it.collisionMax.X < collisionMax.X &&
                it.collisionMax.Y >= collisionMin.Y &&
                it.collisionMin.Z > collisionMin.Z &&
                it.collisionMax.Z < collisionMax.Z)
            {
                returnList.Add(it);
            }
            if (!qhero.isDead &&
                qhero != co &&
                qhero.collisionMin.X > collisionMin.X &&
                qhero.collisionMax.X < collisionMax.X &&
                qhero.collisionMax.Y >= collisionMin.Y &&
                qhero.collisionMin.Z > collisionMin.Z &&
                qhero.collisionMax.Z < collisionMax.Z)
            {
                returnList.Add(qhero);
            }
            if (steve != co &&
                steve.collisionMin.X > collisionMin.X &&
                steve.collisionMax.X < collisionMax.X &&
                steve.collisionMax.Y >= collisionMin.Y &&
                steve.collisionMin.Z > collisionMin.Z &&
                steve.collisionMax.Z < collisionMax.Z)
            {
                returnList.Add(steve);
            }
        }
        return returnList;
    }

    /**
     * Return a list of objects that are contained within the given object.
     * Contained means the object is within the X and Z bounds of our object, and
     * is touching our object on the Y axis.
     */
    public static List<CollidableObject> containedBy(CollidableObject co)
    {
        Vector3 collisionMin = co.collisionMin;
        Vector3 collisionMax = co.collisionMax;
        QuinHero qhero = ((LevelBase)Game.getGame().getEnvStack().currentEnv()).m_qhero;
        Steve steve = ((LevelBase)Game.getGame().getEnvStack().currentEnv()).m_steve;
        List<CollidableObject> returnList = new List<CollidableObject>();
        foreach (CollidableObject it in ((LevelBase)Game.getGame().getEnvStack().currentEnv()).m_objectList)
        {
            if (it != co &&
                collisionMin.X > it.collisionMin.X &&
                collisionMax.X < it.collisionMax.X &&
                collisionMax.Y >= it.collisionMin.Y &&
                collisionMin.Z > it.collisionMin.Z &&
                collisionMax.Z < it.collisionMax.Z)
            {
                returnList.Add(it);
            }
            if (!qhero.isDead &&
                qhero != co &&
                collisionMin.X > qhero.collisionMin.X &&
                collisionMax.X < qhero.collisionMax.X &&
                collisionMax.Y >= qhero.collisionMin.Y &&
                collisionMin.Z > qhero.collisionMin.Z &&
                collisionMax.Z < qhero.collisionMax.Z)
            {
                returnList.Add(qhero);
            }
            if (steve != co &&
                collisionMin.X > steve.collisionMin.X &&
                collisionMax.X < steve.collisionMax.X &&
                collisionMax.Y >= steve.collisionMin.Y &&
                collisionMin.Z > steve.collisionMin.Z &&
                collisionMax.Z < steve.collisionMax.Z)
            {
                returnList.Add(steve);
            }
        }
        return returnList;
    }

    /**
     * Goes through both SP list and living object list, finds positions of all
     * objects of the given type, and returns that list.
     */
    public List<Vector3> getPositionsFor(ObjectFactory.TypeEnum type)
    {
        List<Vector3> list = new List<Vector3>();

        foreach (SpawnPoint sp in m_SPList)
        {
            if (sp.isActive() && sp.getType() == type)
            {
                list.Add(sp.getPosition());
            }
        }
        foreach (CollidableObject co in m_objectList)
        {
            if (co.myType.myType == type)
            {
                list.Add(co.position);
            }
        }

        return list;
    }

    /**
     * Our update function.
     * Updates the lists of all entities/etc
     * that exist within the level.
     * @param
     */
    override public void update(double dt)
    {
        /*
         * Update in this order:
         *      Iterate SpawnPoints
         *      Inputs
         *      Player
         *      All other entities
         *      Camera
         *      HUD
         */

        // Update our quest tracker. If we've completed all quests, go to the last level
        m_goalTrigger.update(dt);
        if(m_goalTrigger.isEmpty())
        {
            m_parentStack.popOnce(new FinalLevelBase());
        }
        
        // Iterate SPs, spawn if needed
        // Should probably make this only check SPs within our current spawnBounds,
        // rather than iterating over _all_ SPs for no raisin.
        foreach(SpawnPoint sp in m_SPList)
        {
            // TODO: Probably replace this isOnScreen call with an "isInSpawnBounds"
            // or other such call
            //sorry ed, i modified this for testing my MissionObjects - quin
            if(sp.isActive() && Camera.isNowOnScreen(sp.getWorldBounds()))
            {
                CollidableObject temp = sp.spawn();
                if (temp is MissionObject)
                {
                    ((MissionObject)temp).setHighlighting(true);
                }
                m_objectList.Add(temp);
            }
        }
        
        // Update inputs
        InputManager.getSingleton().update();
        // Special cases for input checking
#if DEBUG
        if(InputManager.getSingleton().getJustPressedControlState(InputManager.BACK_BUTTON))
        {
            m_parentStack.popAll(new MenuBase());
        }
#endif
        if (InputManager.getSingleton().getJustPressedControlState(InputManager.PAUSE_BUTTON))
        {
            m_parentStack.push(new PauseEnv());
        }

        // Player inputs
        //m_hero.update(dt);
        if (waitingRes)
        {
            if (m_prof.isResurrected())
            {
                m_qhero.reset(heroStart);
                m_qhero.update(dt);
                waitingRes = false;
            }
        }
        else if (m_qhero.isDead)
        {
            deadSince += dt;
            if (deadSince > deadWait)
            {
                resetHero();
            }
        }
        else
        {
            m_qhero.update(dt);
        }
        m_steve.update(dt);

        //list to store cullable objects
        List<CollidableObject> cullable = new List<CollidableObject>();
        // Update all other entities
        foreach(CollidableObject co in m_objectList)
        {
            co.update(dt);
            if (co.isCullable)
            {
                cullable.Add(co);
            }
        }
        //and remove all objects that have requested to be culled
        foreach (CollidableObject co in cullable)
        {
            m_objectList.Remove(co);
        }
        foreach (CollidableObject co in m_additionList)
        {
            m_objectList.Add(co);
        }
        m_additionList.Clear();

        // Update camera
        m_camera.update(dt);
        if (m_steve.hitGround)
        {
            float distance = Math.Abs(m_steve.position.X - m_qhero.position.X);
            ((FollowerCamera)m_camera).shakeFor(distance, 1000);
            if (distance < 1100)
            {
                Game.getGame().getSoundManager().playOneSoundEffect(((AudioAsset)Game.getGame().getAssetLibrary().getAudio(AssetLibrary.audioType.SOUND_EFFECT).get("footstep_close")).sound, 1.0f);
            }
            else
            {
                Game.getGame().getSoundManager().playOneSoundEffect(((AudioAsset)Game.getGame().getAssetLibrary().getAudio(AssetLibrary.audioType.SOUND_EFFECT).get("footstep_far")).sound, 1.0f);
            }
        }

        // Update HUD
        m_hud.update(dt);

        // ROAR!
        roarTracker(dt);
    }

    private void roarTracker(double dt)
    {
        currentTime += dt;
        if (currentTime >= roarTime)
        {
            currentTime = 0;
            playRoar();
        }
        
    }

    private void playRoar()
    {
        if (currentRoar == 0)
        {
            currentRoar++;
            Game.getGame().getSoundManager().playOneSoundEffect(((AudioAsset)Game.getGame().getAssetLibrary().getAudio(AssetLibrary.audioType.SOUND_EFFECT).get("steve_longRoar1")).sound, 1.0f);
        } else if (currentRoar == 1)
        {
            currentRoar++;
            Game.getGame().getSoundManager().playOneSoundEffect(((AudioAsset)Game.getGame().getAssetLibrary().getAudio(AssetLibrary.audioType.SOUND_EFFECT).get("steve_longRoar2")).sound, 0.7f);
        } else if (currentRoar == 2)
        {
            currentRoar=0;
            Game.getGame().getSoundManager().playOneSoundEffect(((AudioAsset)Game.getGame().getAssetLibrary().getAudio(AssetLibrary.audioType.SOUND_EFFECT).get("steve_quickRoar")).sound, 0.7f);
        } 
        
    }

    private void resetHero()
    {
        if (m_prof != null && m_incubator != null)
        {
            heroStart = m_prof.resurrect(m_qhero, m_incubator);
            m_qhero.setPosition(heroStart);
            waitingRes = true;Game.getGame().getSoundManager().playOneSoundEffect(((AudioAsset)Game.getGame().getAssetLibrary().getAudio(AssetLibrary.audioType.SOUND_EFFECT).get("prof_facePalm")).sound, 1.0f);
            m_prof.startResurrection();
        }
        else
        {
            m_qhero.reset(heroStart);
        }
        deadSince = 0;
            
    }

    /**
     * Our render function.
     * Draws all entities/etc that exist
     * within the level.
     */
    override public void render()
    {
        /*
         * Render in this order:
         *      All other entities
         *      Player
         *      HUD
         */

        // Render all other entities
        foreach (CollidableObject i in m_objectList)
        {
            i.draw();
        }
        if (!m_qhero.isDead)
        {
            m_qhero.draw();
        }
        m_steve.draw();
        m_goalTrigger.draw();

        // Player render
        //m_hero.draw();

        // Render HUD
        m_hud.render();
    }
}
